_See Readme for installation instructions_
Santé Publique France (SPF) souhaite mettre à disposition de ses agents des informations plus claires, lisibles et accessibles que les données brutes disponibles. Nous allons ici étudier les données Open Food Facts afin de les aider à mieux observer et comprendre quels sont les enjeux de santé publique liés aux produits alimentaires de la grande distribution.
L'objectif est donc ici de produire des analyses graphiques parlantes au plus grand nombre et pertinentes du point de vue des problématiques de santé publique.
Nous allons utiliser le langage Python, et présenter ici le code, les résultats et l'analyse sous forme de Notebook JupyterLab.
Nous allons aussi utiliser les bibliothèques usuelles d'exploration et analyse de données, afin d'améliorer la simplicité et la performance de notre code :
Les données mises à disposition sont issues de Open Food Facts et présentent les données sur les produits alimentaires.
Nous allons télécharger et extraire le fichier ZIP, puis effectuer une première passe afin de traiter les irrégularités du fichier, avant de charger les données et observer quelques valeurs.
Le fichier contenant les données est mal formé à plusieurs endroits : des sauts de ligne sont présents dans 23 lignes à la fin de la colonne first_packaging_code_geo. Ces irrégularités sont facilement repérables car ce sont les seules lignes qui ne commencent pas par le code de l'article (code), mais par un séparateur \t. Nous allons donc corriger ces irrégularités en supprimant les sauts de ligne superflus, puis écrire les données propres dans un nouveau fichier CSV.
Nous allons charger les données en mémoire et convertir les valeurs dans le bon type, selon les spécifications fournies.
<class 'pandas.core.frame.DataFrame'> RangeIndex: 320749 entries, 0 to 320748 Columns: 162 entries, code to water-hardness_100g dtypes: Int64(5), datetime64[ns](2), float64(99), object(56) memory usage: 398.0+ MB
Le fichier de données fourni contient 162 variables pour 320749 individus.
Nous allons chercher à n'utiliser que les variables pertinentes pour SPF : celles pour lesquelles nous avons suffisament de valeurs non vides pour pouvoir faire une analyse statistique fiable, et qui peuvent avoir un réel sens du point de vue des problématiques de santé publique.
Nous voyons que un grand nombre de variables ont un taux de complétude très faible et ne seront donc pas utilisables. Nous allons donc restreindre notre analyse aux variables utilisées pour le calcul du Nutri Score, qui est un indicateur très parlant du point de vue de la santé.
Affichons quelques informations et les premières valeurs observées.
<class 'pandas.core.frame.DataFrame'> RangeIndex: 320749 entries, 0 to 320748 Data columns (total 13 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 code 320749 non-null object 1 product_name 302987 non-null object 2 main_category 84389 non-null object 3 additives_n 248961 non-null Int64 4 nutrition_grade_fr 221233 non-null object 5 nutrition-score-fr_100g 221233 non-null float64 6 energy_100g 261136 non-null float64 7 saturated-fat_100g 229577 non-null float64 8 sugars_100g 244994 non-null float64 9 salt_100g 255533 non-null float64 10 fruits-vegetables-nuts_100g 3046 non-null float64 11 fiber_100g 200891 non-null float64 12 proteins_100g 259929 non-null float64 dtypes: Int64(1), float64(8), object(4) memory usage: 32.1+ MB
| code | product_name | main_category | additives_n | nutrition_grade_fr | nutrition-score-fr_100g | energy_100g | saturated-fat_100g | sugars_100g | salt_100g | fruits-vegetables-nuts_100g | fiber_100g | proteins_100g | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0000000003087 | Farine de blé noir | NaN | <NA> | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| 1 | 0000000004530 | Banana Chips Sweetened (Whole) | NaN | 0 | d | 14.0 | 2243.0 | 28.57 | 14.29 | 0.00000 | NaN | 3.6 | 3.57 |
| 2 | 0000000004559 | Peanuts | NaN | 0 | b | 0.0 | 1941.0 | 0.00 | 17.86 | 0.63500 | NaN | 7.1 | 17.86 |
| 3 | 0000000016087 | Organic Salted Nut Mix | NaN | 0 | d | 12.0 | 2540.0 | 5.36 | 3.57 | 1.22428 | NaN | 7.1 | 17.86 |
| 4 | 0000000016094 | Organic Polenta | NaN | 0 | NaN | NaN | 1552.0 | NaN | NaN | NaN | NaN | 5.7 | 8.57 |
Voyons quelle est la répartition des différentes variables.
| code | product_name | main_category | additives_n | nutrition_grade_fr | nutrition-score-fr_100g | energy_100g | saturated-fat_100g | sugars_100g | salt_100g | fruits-vegetables-nuts_100g | fiber_100g | proteins_100g | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 320749 | 302987 | 84389 | 248961.000000 | 221233 | 221233.000000 | 2.611360e+05 | 229577.000000 | 244994.000000 | 255533.000000 | 3046.000000 | 200891.000000 | 259929.000000 |
| unique | 320749 | 221343 | 3543 | NaN | 5 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| top | 3248940188021 | Ice Cream | en:beverages | NaN | d | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| freq | 1 | 410 | 6054 | NaN | 62763 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| mean | NaN | NaN | NaN | 1.936384 | NaN | 9.164736 | 1.141842e+03 | 5.129562 | 16.002983 | 2.028455 | 31.376615 | 2.862048 | 7.076074 |
| std | NaN | NaN | NaN | 2.502319 | NaN | 9.055796 | 6.446875e+03 | 8.013929 | 22.326319 | 128.263683 | 31.947699 | 12.867424 | 8.408824 |
| min | NaN | NaN | NaN | 0.000000 | NaN | -15.000000 | 0.000000e+00 | 0.000000 | -17.860000 | 0.000000 | 0.000000 | -6.700000 | -800.000000 |
| 25% | NaN | NaN | NaN | 0.000000 | NaN | 1.000000 | 3.770000e+02 | 0.000000 | 1.300000 | 0.063500 | 0.000000 | 0.000000 | 0.700000 |
| 50% | NaN | NaN | NaN | 1.000000 | NaN | 10.000000 | 1.100000e+03 | 1.790000 | 5.710000 | 0.581660 | 22.100000 | 1.500000 | 4.760000 |
| 75% | NaN | NaN | NaN | 3.000000 | NaN | 16.000000 | 1.674000e+03 | 7.140000 | 24.000000 | 1.374140 | 50.950000 | 3.600000 | 10.000000 |
| max | NaN | NaN | NaN | 31.000000 | NaN | 40.000000 | 3.251373e+06 | 550.000000 | 3520.000000 | 64312.800000 | 100.000000 | 5380.000000 | 430.000000 |
Nous voyons déjà qu'il y a une répartition très inégale des catégories de produits et qu'il faudrait certainement améliorer la catégorisation afin d'éviter d'avoir un très grand nombre de catégories à 1 seul élément.
Nous voyons que les 20 catégories les plus représentées représentent près de 50% de toutes les valeurs. De même, les 5 premières catégories représentent plus de 80% des 3543 valeurs possibles.
nutrition_grade_fr¶Voyons comment sont réparties les notes de Nutri-Score.
Nous voyons que parmis les produits répertoriés, la répartition des produits par Nutri-Score est globalement comparable, avec une sur-représentation du label "D", et une légère sous-représentation du label "B". Nous voyons aussi de quels catégories de produits sont composés chaque scores. Par exemple, il y a beaucoup de conserves parmis les produits de score "A", et de chocolats parmis les produits de score "E".
De la même manière, nous voyons que la plupart des chocolats ont un score de "E" et la plupart des conserves ont un score de "A". Nous mesurerons précisément la corrélation entre les catégories de produits et les notes de Nutri-Score plus loin.
Nous allons dans un premier temps nettoyer les données numérique, avant d'en étudier la répartition. Ceci permettra d'avoir des données plus fiables.
Parmis les données fournies, nous voyons des valeurs négatives pour des variables comme le nombre d'additifs, ou la quantité de sucre pour 100g de produit, ce qui est impossible. Nous allons aussi utiliser la méthode IQR (Inter Quartile Range) pour identifier les données aberrantes (outliers) et les supprimer.
| additives_n | nutrition-score-fr_100g | energy_100g | saturated-fat_100g | sugars_100g | salt_100g | fruits-vegetables-nuts_100g | fiber_100g | proteins_100g | |
|---|---|---|---|---|---|---|---|---|---|
| count | 248961.000000 | 221233.000000 | 2.611360e+05 | 229577.000000 | 244987.000000 | 255533.000000 | 3046.000000 | 200890.000000 | 259926.000000 |
| mean | 1.936384 | 9.164736 | 1.141842e+03 | 5.129562 | 16.003589 | 2.028455 | 31.376615 | 2.862096 | 7.081171 |
| std | 2.502319 | 9.055796 | 6.446875e+03 | 8.013929 | 22.326329 | 128.263683 | 31.947699 | 12.867438 | 8.198381 |
| min | 0.000000 | -15.000000 | 0.000000e+00 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
| 25% | 0.000000 | 1.000000 | 3.770000e+02 | 0.000000 | 1.300000 | 0.063500 | 0.000000 | 0.000000 | 0.700000 |
| 50% | 1.000000 | 10.000000 | 1.100000e+03 | 1.790000 | 5.710000 | 0.581660 | 22.100000 | 1.500000 | 4.760000 |
| 75% | 3.000000 | 16.000000 | 1.674000e+03 | 7.140000 | 24.000000 | 1.374140 | 50.950000 | 3.600000 | 10.000000 |
| max | 31.000000 | 40.000000 | 3.251373e+06 | 550.000000 | 3520.000000 | 64312.800000 | 100.000000 | 5380.000000 | 430.000000 |
Nous avons bien supprimé les valeurs négatives impossibles, mais nous voyons encore des valeurs maximum aberrantes (ex. : 550g d'acides gras saturés pour 100g de produit). Nous allons donc supprimer les valeurs aberrantes restantes grâce à la méthode IQR.
| additives_n | nutrition-score-fr_100g | energy_100g | saturated-fat_100g | sugars_100g | salt_100g | fruits-vegetables-nuts_100g | fiber_100g | proteins_100g | |
|---|---|---|---|---|---|---|---|---|---|
| count | 239154.000000 | 221229.000000 | 260056.000000 | 212330.000000 | 229679.000000 | 239046.000000 | 3046.000000 | 186763.000000 | 247336.000000 |
| mean | 1.601332 | 9.164178 | 1114.283467 | 3.502375 | 12.130071 | 0.718235 | 31.376615 | 1.914724 | 5.887857 |
| std | 1.832342 | 9.054928 | 781.784025 | 4.548240 | 15.161208 | 0.774207 | 31.947699 | 2.213743 | 5.998511 |
| min | 0.000000 | -15.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
| 25% | 0.000000 | 1.000000 | 372.000000 | 0.000000 | 1.080000 | 0.048260 | 0.000000 | 0.000000 | 0.500000 |
| 50% | 1.000000 | 10.000000 | 1094.000000 | 1.400000 | 4.800000 | 0.474980 | 22.100000 | 1.200000 | 4.230000 |
| 75% | 3.000000 | 16.000000 | 1674.000000 | 5.700000 | 18.400000 | 1.186180 | 50.950000 | 3.300000 | 9.000000 |
| max | 7.000000 | 38.000000 | 3619.000000 | 17.840000 | 58.000000 | 3.340000 | 100.000000 | 9.000000 | 23.940000 |
Nous avons maintenant des valeurs qui semblent correctes. Observons leur répartition avant de compléter les valeurs vides.
Nous voyons qu'il y a encore des données aberrantes, notamment en analysant les données selon la note de Nutri-Score.
Nous allons donc à nouveau supprimer ces données aberrantes grâce à la fonction remove_outliers() définie.
| additives_n | nutrition-score-fr_100g | energy_100g | saturated-fat_100g | sugars_100g | salt_100g | fruits-vegetables-nuts_100g | fiber_100g | proteins_100g | |
|---|---|---|---|---|---|---|---|---|---|
| count | 233835.000000 | 219602.000000 | 254958.000000 | 206037.000000 | 224249.000000 | 238747.000000 | 3043.000000 | 182606.000000 | 240982.000000 |
| mean | 1.525738 | 9.152203 | 1106.116924 | 3.497238 | 11.791241 | 0.716328 | 31.308962 | 1.789232 | 5.506563 |
| std | 1.766967 | 9.003385 | 770.379808 | 4.587804 | 14.999885 | 0.772293 | 31.890646 | 2.068080 | 5.579444 |
| min | 0.000000 | -9.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
| 25% | 0.000000 | 1.000000 | 379.000000 | 0.000000 | 1.000000 | 0.046200 | 0.000000 | 0.000000 | 0.500000 |
| 50% | 1.000000 | 10.000000 | 1084.000000 | 1.200000 | 4.500000 | 0.470000 | 22.000000 | 1.200000 | 4.000000 |
| 75% | 2.000000 | 16.000000 | 1661.000000 | 5.810000 | 17.650000 | 1.181100 | 50.550000 | 3.000000 | 8.400000 |
| max | 7.000000 | 30.000000 | 3615.000000 | 17.810000 | 58.000000 | 3.337560 | 100.000000 | 9.000000 | 23.940000 |
Nous voyons maintenant que les données sont quasiment toutes contenues dans les "moustaches" des boxplots, autrement dit il n'y a presque plus de données aberrantes.
Il est inutile de conserver des données en doublons, nous allons donc supprimer ces lignes. En l'occurrence, comme le code est unique à chaque produit, il ne peut pas y avoir de lignes en doublon.
count 320749 unique 1 top False freq 320749 dtype: object
Par la suite, toute notre analyse va se baser sur le paramètre nutrition_grade_fr. Nous allons supprimer les lignes où la valeur de cette variable est vide.
Nous pouvons alors considérer les données restantes comme fiables, et nous allons nous baser sur ces valeurs pour extrapoler les valeurs manquantes dans notre jeu de données. Nous allons remplacer, pour chaque valeur numérique vide, la valeur moyenne des individus ayant la même note de Nutri-Score.
Nous voyons que nous n'avons plus de valeurs vides pour toutes les variables numériques sélectionnées.
Maintenant nos données nettoyées, nous allons l'exploiter afin d'observer et mesurer les tendances significatives au sein du jeu de données.
Nous allons ici analyser l'influence réciproque des deux variables catégoriques : top_category et nutrition_grade_fr.
Ce tableau nous indique les corrélations fortes entre les catégories de produits et le Nutri-Scores.
Nous voyons déjà que parmis les 20 types de produits les plus représentés, ceux ayant un meilleur Nutri-Score sont les produits en boîte, les produits à base de plantes et les pâtes. Les légumes frais sont le plus souvent de Nutri-Score A, tandis que les chocolats, bonbons, biscuits et en-cas sucrés ont le plus souvent un mauvais Nutri-Score.
Nous allons ici analyser l'influence réciproque des variables numériques prises en compte dans le calcul du Nutri-Score et la note de Nutri-Score.
Nous voyons que, suite à l'imputation des valeurs vides, les médianes et les quartiles se sont rapprochés de la moyenne, ce qui fait apparaitre de nouveaux outliners, que nous allons conserver.
Il faut noter le cas particulier de la variable fruits-vegetables-nuts_100g pour laquelle nous avons remplacé les 98,7% de valeurs vides par la moyenne des 1,3% de valeurs non vides. La variance est donc très faible pour cette variable et cette imputation pourrait entraîner des résultats biaisés par la suite.
Nous allons chercher à quantifier la corrélation entre chacune de ces valeurs nutritives et la note de Nutri-Score. Pour celà, nous allons effectuer une analyse de la variance (ANOVA) entre les variables numériques (valeurs nutritives) et les catégories (notes Nutri-Score).
Nous voyons ici qu'il y a une corrélation presque parfaite () entre nutrition-score-fr_100g et nutrition_grade_fr : ceci est attendu, puisque la note de Nutri-Score est défini linéairement à partir du Nutri-Score. Le fait que la corrélation ne soit pas de 1 montre qu'il y a des erreurs ou exceptions qu'il faudrait corriger ou expliquer.
Nous observons une très forte corrélation () entre fruits-vegetables-nuts_100g et nutrition_grade_fr : ceci s'explique principalemet par l'imputation faite par la moyenne sur la plupart des valeurs de cette variable. Ce résultat n'est donc pas fiable et ne permet pas de tirer de conclusion.
Nous voyons ensuite que les variables ayant la plus grande corrélation avec le Nutri-Score sont les valeurs nutritives influencant négativement le Nutri-Score : la densité d'énergie, les gresses saturées et les sucres.
Nous allons ici analyser l'influence réciproque des variables numériques prises en compte dans le calcul du Nutri-Score et les trois catégories de produits les plus représentées.
Cette analyse nous permet de confirmer les corrélations observées précédemment : les chocolats ont généralement des valeurs élevées d'énergie, de gras et de sucre, et un Nutri-Score plus élevé que le reste des produits.
Nous allons ici analyser l'influence réciproque des variables numériques prises en compte dans le calcul du Nutri-Score. Nous allons observer séparément les variables qui doivent avoir une influence positive sur le Nutri-Score, puis les variables devant avoir une influence négative.
Nous voyons ici qu'il semble y avoir une corrélation positive entre les variables energy, sugars et fat.
La corrélation positive entre le Nutri-Score et toutes les variables listées ici est aussi assez visible.
Il est ici assez difficile de distingueer une corrélation claire entre les différentes variables.
Noous allons devoir recourrir à une mesure mathématique des corrélations afin de les quantifier et les apprécier réellement.
Nous voyons nettement ici qu'il y a une forte corrélation positive entre les produits à forte densité énergétique et gras , ainsi qu'une forte corrélation négative entre les produits gras et à forte teneur en fruits, légules et noix.
Nous allons ici tenter de modéliser nos différents produits selont leurs valeurs nutritives, et comparer ce modèle aux notes de Nutri-Score, ainsi qu'aux catégories de produits afin d'essayer de généraliser des tendances observables.
Dans en premier temps, nous allons normaliser chacune des variables numériques afin de mieux pouvoir les comparer. Nous en profitons pour modéliser (via une régression linéaire) comment sont corrélées chacune des valeurs nutritives avec le Nutri-Score.
Nous voyons ici que la variable ayant la plus grande influence positive est le taux de graisses saturées ( , ). A contrario, le taux de fruits-legumes-noix a une très forte influence négative sur le Nutri-Score ( , ). Les taux de protéines a en reanche très peu de corrélation avec le Nutri-Score ( ).
Nous allons maintenant chercher à définir de nouvelles variables qui sont des composantes des variables existantes et qui permettent de résumer de manière optimisée les informations contenues dans les variables existantes. Cette méthode s'appelle l'Analyse en Composantes Principales.
Cette nouvelle projection nous permet de visualiser les produits en maximisant la variance dans chaque axe, ce qui permet de bien distinguer chaque individu. Nous observons notamment que selon l'axe "PC1", les produits de Nutri-Score "A" sont bien isolés du reste des points.
Voyons s'il est nécessaire d'ajouter de nouvelles composantes à notre analyse en mesurant la variance des composantes déjà sélectionnées.
Nous voyons que la troisième composante apporte relativement peu de nouvelle information par rapport aux deux premières. Il n'est donc pas nécessaire d'ajouter de nouvelles composantes à notre analyse.
Voyons maintenant comment se répartissent les notes de Nutri-Score , ainsi que les catégories de produits dans nos nouvelles composantes.
Cherchons maintenant à interpréter ces composantes principales en mesurant combien chaque variable initiale contribue à chaque nouvelle composate.
Nous voyons que la composante "PC1" correspond aux produits contenant beaucoup de fruits-legumes-noix et peu de sucres, energie, graisses, et ayant donc un Nutri-Score très faible. La seconde composante correspond aux produits à haute teneur en protéines et fibres, et faible en additifs et sucres. La dernière composante correspond aux produits contenant très peu de sel et plutôt sucrées.
Ces derniers graphiques nous confirment les observations précédentes et nous permettent de généraliser le sens de chaque composantes :